﻿namespace Microsoft.Web.Helpers
{
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Data.Services.Client;
    using System.Dynamic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Web;
    using System.Xml.Linq;

    public static class OData
    {
        private static XNamespace dataServiceNS = XNamespace.Get("http://schemas.microsoft.com/ado/2007/08/dataservices");
        private static XNamespace metadataNS = XNamespace.Get("http://schemas.microsoft.com/ado/2007/08/dataservices/metadata");
        private static XNamespace atomNS = XNamespace.Get("http://www.w3.org/2005/Atom");

        /// <summary>
        /// Creates a new OData query instance pointing to the specified service Uri
        /// </summary>
        /// <param name="serviceUri">The OData service Uri where the queries will be performed</param>
        public static ODataQuery Open(string serviceUri)
        {
            return new ODataQuery(serviceUri);
        }

        /// <summary>
        /// Creates a new OData query instance pointing to the specified service Uri
        /// </summary>
        /// <param name="serviceUri">The OData service Uri where the queries will be performed</param>
        /// <param name="authScheme">The authentication scheme used to connect to the OData service</param>
        public static ODataQuery Open(string serviceUri, IAuthenticationScheme authScheme)
        {
            return new ODataQuery(serviceUri, authScheme);
        }

        /// <summary>
        /// Creates a new instance of a OData Media Link
        /// </summary>
        /// <param name="mediaResource">The stream that represents the Media Resource</param>
        /// <param name="contentType">The content type of the Media Resource associated to the Media Link Entry</param>
        public static dynamic CreateMediaLink(Stream mediaResource, string contentType)
        {
            return new DynamicMediaLink(mediaResource, contentType);
        }

        /// <summary>
        /// Creates a new instance of a OData Entity
        /// </summary>
        public static dynamic CreateEntity()
        {
            return new DynamicEntity();
        }

        /// <summary>
        /// Inserts a new a OData Entity in the specified entity set
        /// </summary>
        /// <param name="entitySetUri">The absolute Uri of the entity set where the new entity will be inserted</param>
        /// <param name="entity">The instance that represents the new entity to be inserted</param>
        public static void Insert(string entitySetUri, DynamicEntity entity)
        {
            Insert(entitySetUri, entity, new AnonymousAuthenticationScheme());
        }

        /// <summary>
        /// Inserts a new a OData Entity in the specified entity set
        /// </summary>
        /// <param name="entitySetUri">The absolute Uri of the entity set where the new entity will be inserted</param>
        /// <param name="entity">The instance that represents the new entity to be inserted</param>
        /// <param name="authScheme">The authentication scheme used to connect to the OData service</param>
        public static void Insert(string entitySetUri, DynamicEntity entity, IAuthenticationScheme authScheme)
        {
            string baseUri;
            string entitySet;
            string entityKey;
            string queryString;
            ValidateServiceUri(entitySetUri, out baseUri, out entitySet, out entityKey, out queryString);

            DataServiceContext context = new DataServiceContext(new Uri(baseUri));
            context.IgnoreMissingProperties = true;

            DataServiceContextHandler handler = new DataServiceContextHandler(authScheme);
            handler.HandleInsert(context);

            EntryProxyObject proxy = CreateEntryProxyObject(entity);
            proxy.Properties = GetProperties(entity);

            context.AddObject(entitySet, proxy);

            if (entity is DynamicMediaLink)
            {
                DynamicMediaLink mediaLink = entity as DynamicMediaLink;
                context.SetSaveStream(proxy, mediaLink.MediaResource, false, mediaLink.ContentType, string.Empty);
            }

            context.SaveChanges();
        }

        /// <summary>
        /// Updates the contents of an existing OData Entity
        /// </summary>
        /// <param name="entityUri">The absolute Uri of the OData entity to be updated</param>
        /// <param name="entity">The instance that represents the entity to be updated</param>
        public static void Update(string entityUri, DynamicEntity entity)
        {
            Update(entityUri, entity, string.Empty);
        }

        /// <summary>
        /// Updates the contents of an existing OData Entity
        /// </summary>
        /// <param name="entityUri">The absolute Uri of the OData entity to be updated</param>
        /// <param name="entity">The instance that represents the entity to be updated</param>
        /// <param name="etag">The etag value of the entity (required for OData services that handles optimistic concurrency in update operations)</param>
        public static void Update(string entityUri, DynamicEntity entity, string etag)
        {
            Update(entityUri, entity, new AnonymousAuthenticationScheme(), etag);
        }

        /// <summary>
        /// Updates the contents of an existing OData Entity
        /// </summary>
        /// <param name="entityUri">The absolute Uri of the OData entity to be updated</param>
        /// <param name="entity">The instance that represents the entity to be updated</param>
        /// <param name="authScheme">The authentication scheme used to connect to the OData service</param>
        public static void Update(string entityUri, DynamicEntity entity, IAuthenticationScheme authScheme)
        {
            Update(entityUri, entity, authScheme, string.Empty);
        }

        /// <summary>
        /// Updates the contents of an existing OData Entity
        /// </summary>
        /// <param name="entityUri">The absolute Uri of the OData entity to be updated</param>
        /// <param name="entity">The instance that represents the entity to be updated</param>
        /// <param name="authScheme">The authentication scheme used to connect to the OData service</param>
        /// <param name="etag">The etag value of the entity (required for OData services that handles optimistic concurrency in update operations)</param>
        public static void Update(string serviceUri, DynamicEntity entity, IAuthenticationScheme authScheme, string etag)
        {
            string baseUri;
            string entitySet;
            string entityKey;
            string queryString;
            ValidateServiceUri(serviceUri, out baseUri, out entitySet, out entityKey, out queryString);

            DataServiceContext context = new DataServiceContext(new Uri(baseUri));
            context.IgnoreMissingProperties = true;

            DataServiceContextHandler handler = new DataServiceContextHandler(authScheme);
            handler.HandleUpdate(context);

            EntryProxyObjectWithKey proxy = CreateEntryProxyObjectWithKey(entity);
            proxy.Key = entityKey;
            proxy.Properties = GetProperties(entity);

            context.AttachTo(entitySet, proxy, etag);
            context.UpdateObject(proxy);
            
            if (entity is DynamicMediaLink)
            {
                DynamicMediaLink mediaLink = entity as DynamicMediaLink;
                context.SetSaveStream(proxy, mediaLink.MediaResource, false, mediaLink.ContentType, string.Empty);
            }            

            context.SaveChanges();
        }

        /// <summary>
        /// Deletes an existing OData Entity
        /// </summary>
        /// <param name="entityUri">The absolute Uri of the OData entity to be deleted</param>
        public static void Delete(string entityUri)
        {
            Delete(entityUri, string.Empty);
        }

        /// <summary>
        /// Deletes an existing OData Entity
        /// </summary>
        /// <param name="entityUri">The absolute Uri of the OData entity to be deleted</param>
        /// <param name="etag">The etag value of the entity (required for OData services that handles optimistic concurrency in delete operations)</param>
        public static void Delete(string serviceUri, string etag)
        {
            Delete(serviceUri, new AnonymousAuthenticationScheme(), etag);
        }

        /// <summary>
        /// Deletes an existing OData Entity
        /// </summary>
        /// <param name="entityUri">The absolute Uri of the OData entity to be deleted</param>
        /// <param name="authScheme">The authentication scheme used to connect to the OData service</param>
        public static void Delete(string entityUri, IAuthenticationScheme authScheme)
        {
            Delete(entityUri, authScheme, string.Empty);
        }

        /// <summary>
        /// Deletes an existing OData Entity
        /// </summary>
        /// <param name="entityUri">The absolute Uri of the OData entity to be deleted</param>
        /// <param name="authScheme">The authentication scheme used to connect to the OData service</param>
        /// <param name="etag">The etag value of the entity (required for OData services that handles optimistic concurrency in delete operations)</param>
        public static void Delete(string entityUri, IAuthenticationScheme authScheme, string etag)
        {
            string baseUri;
            string entitySet;
            string entityKey;
            string queryString;
            ValidateServiceUri(entityUri, out baseUri, out entitySet, out entityKey, out queryString);

            DataServiceContext context = new DataServiceContext(new Uri(baseUri));

            DataServiceContextHandler handler = new DataServiceContextHandler(authScheme);
            handler.HandleDelete(context);

            EntryProxyObject proxy = new EntryProxyObjectWithKey() { Key = entityKey };
            context.AttachTo(entitySet, proxy, etag);
            context.DeleteObject(proxy);

            context.SaveChanges();
        }

        /// <summary>
        /// Performs a request to the OData Service Uri returning a dynamic list with the results
        /// </summary>
        /// <param name="serviceUri">The OData service uri to be requested</param>
        public static IList<dynamic> Get(string serviceUri)
        {
            return Get(serviceUri, string.Empty);
        }

        /// <summary>
        /// Performs a request to the OData Service Uri returning a dynamic list with the results
        /// </summary>
        /// <param name="serviceUri">The OData service uri to be requested</param>
        /// <param name="authScheme">The authentication scheme used to connect to the OData service</param>
        public static IList<dynamic> Get(string serviceUri, IAuthenticationScheme authScheme)
        {
            return Get(serviceUri, new NameValueCollection(), authScheme);
        }

        /// <summary>
        /// Performs a request to the OData Service Uri returning a dynamic list with the results
        /// </summary>
        /// <param name="serviceUri">The OData service Uri to be requested</param>
        /// <param name="queryOptions">A string that represents the query options to be applied in the data service request</param>
        public static IList<dynamic> Get(string serviceUri, string queryOptions)
        {
            return Get(serviceUri, queryOptions, new AnonymousAuthenticationScheme());
        }

        /// <summary>
        /// Performs a request to the OData Service Uri returning a dynamic list with the results
        /// </summary>
        /// <param name="serviceUri">The OData service uri to be requested</param>
        /// <param name="queryOptions">A string that represents the query options to be applied in the data service request</param>
        /// <param name="authScheme">The authentication scheme used to connect to the OData service</param>
        public static IList<dynamic> Get(string serviceUri, string queryOptions, IAuthenticationScheme authenticationScheme)
        {
            NameValueCollection options = HttpUtility.ParseQueryString(queryOptions ?? string.Empty);

            return Get(serviceUri, options, authenticationScheme);
        }

        private static EntryProxyObject CreateEntryProxyObject(DynamicEntity entity)
        {
            if (entity is DynamicMediaLink)
            {
                return new MediaLinkEntryProxyObject();
            }
            else
            {
                return new EntryProxyObject();
            }
        }

        private static EntryProxyObjectWithKey CreateEntryProxyObjectWithKey(DynamicEntity entity)
        {
            if (entity is DynamicMediaLink)
            {
                return new MediaLinkEntryProxyObjectWithKey();
            }
            else
            {
                return new EntryProxyObjectWithKey();
            }
        }

        private static IList<dynamic> Get(string serviceUri, NameValueCollection queryOptions, IAuthenticationScheme authenticationScheme)
        {
            string baseUri;
            string entitySet;
            string entityKey;
            string queryString;
            ValidateServiceUri(serviceUri, out baseUri, out entitySet, out entityKey, out queryString);
            string resource = !string.IsNullOrEmpty(entityKey) ? entitySet + "(" + entityKey + ")" : entitySet;

            DataServiceContext context = new DataServiceContext(new Uri(baseUri));
            context.IgnoreMissingProperties = true;

            DataServiceContextHandler handler = new DataServiceContextHandler(authenticationScheme);
            handler.HandleGet(context);

            DataServiceQuery<EntryProxyObject> query = context.CreateQuery<EntryProxyObject>(resource);

            NameValueCollection options = HttpUtility.ParseQueryString(queryString);
            options.Add(queryOptions);

            foreach (string key in options.AllKeys)
            {
                query = query.AddQueryOption(key, options[key]);
            }
            
            QueryOperationResponse<EntryProxyObject> response = query.Execute() as QueryOperationResponse<EntryProxyObject>;

            IList<dynamic> result;
            if (options["$inlinecount"] == "allpages")
            {
                result = new DynamicEntityCollection(response.TotalCount) as IList<dynamic>;
            }
            else
            {
                result = new List<dynamic>();
            }

            foreach (EntryProxyObject proxy in response)
            {
                DynamicEntity entity = new DynamicEntity(proxy.Properties);
                result.Add(entity);
            }

            return result;
        }

        private static void ValidateServiceUri(string serviceUri, out string baseUri, out string resource, out string entitykey, out string queryString)
        {
            string escapedServiceUri = Uri.EscapeUriString(serviceUri);

            // Validate the URI is well-formed
            if (!Uri.IsWellFormedUriString(escapedServiceUri, UriKind.Absolute))
            {
                throw new ArgumentException("The format of the Service Uri is not well-formed: " + escapedServiceUri);
            }

            Uri uri = new Uri(escapedServiceUri);
            baseUri = uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
            string path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
            queryString = uri.GetComponents(UriComponents.Query, UriFormat.Unescaped);

            entitykey = string.Empty;
            resource = path;
            if (path.Contains("(") && path.EndsWith(")"))
            {
                entitykey = GetEntityKey(path);
                resource = resource.Substring(0, path.Length - entitykey.Length - 2);
            }

            if (string.IsNullOrEmpty(resource))
            {
                throw new ArgumentException(@"The Service Uri must include the path to a specific entity set (e.g. http://odataservice.com/<entitySet>).");
            }
        }

        private static string GetEntityKey(string path)
        {
            string[] pathParts = path.Split('(');
            int keyLength = pathParts[pathParts.Length - 1].Length - 1;
            return pathParts[pathParts.Length - 1].Substring(0, keyLength);
        }

        private static Dictionary<string, object> GetProperties(DynamicEntity entity)
        {
            Dictionary<string, object> properties = new Dictionary<string, object>();

            foreach (KeyValuePair<string, object> member in entity.GetDynamicMembers())
            {
                if (member.Value is DynamicEntity)
                {
                    properties.Add(member.Key, GetProperties(member.Value as DynamicEntity));
                }
                else
                {
                    properties.Add(member.Key, member.Value);
                }
            }

            return properties;
        }

        public class ODataQuery
        {
            private string serviceUri;
            private NameValueCollection queryOptions;
            private IAuthenticationScheme authScheme;

            public ODataQuery(string serviceUri)
                : this(serviceUri, new NameValueCollection(), new AnonymousAuthenticationScheme())
            {
            }

            public ODataQuery(string serviceUri, IAuthenticationScheme authScheme)
                : this(serviceUri, new NameValueCollection(), authScheme)
            {                
            }

            private ODataQuery(string serviceUri, NameValueCollection queryOptions)
            {
                this.serviceUri = serviceUri;
                this.queryOptions = queryOptions;
                this.authScheme = new AnonymousAuthenticationScheme();
            }

            private ODataQuery(string serviceUri, NameValueCollection queryOptions, IAuthenticationScheme authScheme)
            {
                this.serviceUri = serviceUri;
                this.queryOptions = queryOptions;
                this.authScheme = authScheme;
            }

            /// <summary>
            /// Creates a new ODataQuery instance with the specified $filter query option
            /// </summary>
            /// <param name="filter">A string that represents the filter option to be applied to the query</param>
            public ODataQuery Where(string filter)
            {
                NameValueCollection options = new NameValueCollection(this.queryOptions);
                options.Add("$filter", filter);

                return new ODataQuery(this.serviceUri, options, this.authScheme);
            }

            /// <summary>
            /// Creates a new ODataQuery instance with the $orderby query option
            /// </summary>
            /// <param name="order">A string that represents the order statement to be applied to the query</param>
            public ODataQuery OrderBy(string order)
            {
                NameValueCollection options = new NameValueCollection(this.queryOptions);
                options.Add("$orderby", order);

                return new ODataQuery(this.serviceUri, options, this.authScheme);
            }

            /// <summary>
            /// Creates a new ODataQuery instance with the $skip query option
            /// </summary>
            /// <param name="count">Integer value that represents the number of entries to seek</param>
            public ODataQuery Skip(int count)
            {
                NameValueCollection options = new NameValueCollection(this.queryOptions);
                options.Add("$skip", count.ToString());

                return new ODataQuery(this.serviceUri, options, this.authScheme);
            }

            /// <summary>
            /// Creates a new ODataQuery instance with the $top query option
            /// </summary>
            /// <param name="count">Integer value that represents the number of entries to retrieve</param>
            public ODataQuery Top(int count)
            {
                NameValueCollection options = new NameValueCollection(this.queryOptions);
                options.Add("$top", count.ToString());

                return new ODataQuery(this.serviceUri, options, this.authScheme);
            }

            /// <summary>
            /// Creates a new ODataQuery instance with the $inlinecount query option value set to "allpages"
            /// </summary>
            public ODataQuery IncludeTotalCount()
            {
                NameValueCollection options = new NameValueCollection(this.queryOptions);
                options.Add("$inlinecount", "allpages");

                return new ODataQuery(this.serviceUri, options, this.authScheme);
            }

            /// <summary>
            /// Performs a request to the OData Service returning a dynamic list with the results
            /// </summary>
            public IList<dynamic> Get()
            {                
                return OData.Get(this.serviceUri, this.queryOptions, this.authScheme);
            }
        }

        public class DynamicEntityCollection : List<dynamic>
        {
            public DynamicEntityCollection(long totalCount)
                : base()
            {
                this.TotalCount = totalCount;
            }

            public long TotalCount
            {
                get;
                private set;
            }
        }

        public class DynamicEntity : DynamicObject
        {
            private IDictionary<string, object> dictionary;

            public DynamicEntity()
            {
                this.dictionary = new Dictionary<string, object>();
            }

            public DynamicEntity(IDictionary<string, object> valuesDictionary)
            {
                this.dictionary = new Dictionary<string, object>();

                foreach (KeyValuePair<string, object> keyValue in valuesDictionary)
                {
                    IDictionary<string, object> innerDictionary = keyValue.Value as Dictionary<string, object>;
                    if (innerDictionary != null)
                    {
                        this.dictionary.Add(keyValue.Key, new DynamicEntity(innerDictionary));
                    }
                    else
                    {
                        this.dictionary.Add(keyValue);
                    }
                }                                                    
            }
         
            public int Count
            {
                get
                {
                    return this.dictionary.Count;
                }
            }

            public override IEnumerable<string> GetDynamicMemberNames()
            {
                return this.dictionary.Keys;
            }

            public IDictionary<string, object> GetDynamicMembers()
            {
                return this.dictionary;
            }

            public override bool TryGetMember(GetMemberBinder binder, out object result)
            {
                string name = binder.Name;
                return this.dictionary.TryGetValue(name, out result);
            }

            public override bool TrySetMember(SetMemberBinder binder, object value)
            {
                this.dictionary[binder.Name] = value;
                return true;
            }
        }

        public class DynamicMediaLink : DynamicEntity
        {
            public DynamicMediaLink(Stream mediaResource, string contentType) : base()
            {
                this.MediaResource = mediaResource;
                this.ContentType = contentType;
            }

            public Stream MediaResource { get; private set; }

            public string ContentType { get; private set; }
        }

        [Serializable]
        public class ODataAtomFormatException : Exception
        {
            public ODataAtomFormatException(string message)
                : base(message)
            {
            }
        }

        [global::System.Data.Services.Common.DataServiceEntityAttribute()]
        public class EntryProxyObject
        {
            public Dictionary<string, object> Properties { get; set; }
        }

        [global::System.Data.Services.Common.DataServiceKey("Key")]
        public class EntryProxyObjectWithKey : EntryProxyObject
        {
            public string Key { get; set; }
        }

        [global::System.Data.Services.Common.HasStreamAttribute()]
        public class MediaLinkEntryProxyObjectWithKey : EntryProxyObjectWithKey
        {
        }

        [global::System.Data.Services.Common.HasStreamAttribute()]
        private class MediaLinkEntryProxyObject : EntryProxyObject
        {
        }

        private class DataServiceContextHandler
        {
            private IAuthenticationScheme authScheme;

            public DataServiceContextHandler(IAuthenticationScheme authScheme)
            {
                this.authScheme = authScheme;
            }

            public void HandleGet(DataServiceContext context)
            {
                context.ReadingEntity += new EventHandler<ReadingWritingEntityEventArgs>(this.ReadEntity);
                context.SendingRequest += new EventHandler<SendingRequestEventArgs>(this.SignRequest);
            }

            public void HandleUpdate(DataServiceContext context)
            {
                context.WritingEntity += new EventHandler<ReadingWritingEntityEventArgs>(this.WriteEntity);
                context.SendingRequest += new EventHandler<SendingRequestEventArgs>(this.UpdateRequestUri);
                context.SendingRequest += new EventHandler<SendingRequestEventArgs>(this.SignRequest);                
            }

            public void HandleDelete(DataServiceContext context)
            {
                context.SendingRequest += new EventHandler<SendingRequestEventArgs>(this.UpdateRequestUri);
                context.SendingRequest += new EventHandler<SendingRequestEventArgs>(this.SignRequest);
            }

            public void HandleInsert(DataServiceContext context)
            {
                context.WritingEntity += new EventHandler<ReadingWritingEntityEventArgs>(this.WriteEntity);
                context.SendingRequest += new EventHandler<SendingRequestEventArgs>(this.SignRequest);
            }

            private static object GetPropertyValue(XElement property, XNamespace metadataNamespace)
            {
                XAttribute nullAttribute = property.Attribute(metadataNamespace + "null");
                XAttribute typeAttribute = property.Attribute(metadataNamespace + "type");

                if (nullAttribute != null && nullAttribute.Value == "true")
                {
                    return null;
                }

                if (typeAttribute == null)
                {
                    return property.Value;
                }

                switch (typeAttribute.Value)
                {
                    case "Edm.Boolean":
                        return Boolean.Parse(property.Value);
                    case "Edm.Byte":
                        return Byte.Parse(property.Value);
                    case "Edm.DateTime":
                    case "Edm.Time":
                        return DateTime.Parse(property.Value);
                    case "Edm.Decimal":
                        return Decimal.Parse(property.Value);
                    case "Edm.Double":
                        return Double.Parse(property.Value);
                    case "Edm.Single":
                        return Single.Parse(property.Value);
                    case "Edm.Guid":
                        return Guid.Parse(property.Value);
                    case "Edm.Int16":
                        return Int16.Parse(property.Value);
                    case "Edm.Int32":
                        return Int32.Parse(property.Value);
                    case "Edm.Int64":
                        return Int64.Parse(property.Value);
                    case "Edm.SByte":
                        return SByte.Parse(property.Value);
                    case "Edm.DateTimeOffset":
                        return DateTimeOffset.Parse(property.Value);
                    default:
                        if (property.HasElements)
                        {
                            return GetComplexPropertyAsDictionary(property);
                        }

                        return property.Value;
                }
            }

            private static IDictionary<string, object> GetComplexPropertyAsDictionary(XElement property)
            {
                IDictionary<string, object> complexProperty = new Dictionary<string, object>();

                foreach (XElement element in property.Elements())
                {
                    complexProperty.Add(new KeyValuePair<string, object>(element.Name.LocalName, GetPropertyValue(element, metadataNS)));
                }

                return complexProperty;
            }

            private static void SetPropertiesFromDictionary(XElement properties, Dictionary<string, object> dictionary)
            {
                foreach (KeyValuePair<string, object> p in dictionary)
                {
                    if (p.Value is Dictionary<string, object>)
                    {
                        XElement complexProperty = new XElement(dataServiceNS + p.Key);
                        SetPropertiesFromDictionary(complexProperty, p.Value as Dictionary<string, object>);
                        properties.Add(complexProperty);
                    }
                    else
                    {
                        XElement primitiveProperty = new XElement(dataServiceNS + p.Key, p.Value);
                        if (p.Value == null)
                        {
                            primitiveProperty.SetAttributeValue(metadataNS + "null", "true");
                        }

                        properties.Add(primitiveProperty);
                    }
                }
            }

            private void WriteEntity(object sender, ReadingWritingEntityEventArgs e)
            {
                XElement content = e.Data.Element(atomNS + "content");
                XElement properties = (content != null && content.Element(metadataNS + "properties") != null)
                                    ? content.Element(metadataNS + "properties")
                                    : e.Data.Element(metadataNS + "properties");

                if (properties == null)
                {
                    throw new ODataAtomFormatException("Entity properties not found.");
                }

                properties.RemoveAll();
                EntryProxyObject entity = e.Entity as EntryProxyObject;

                SetPropertiesFromDictionary(properties, entity.Properties);
            }

            private void ReadEntity(object sender, ReadingWritingEntityEventArgs e)
            {
                XElement content = e.Data.Element(atomNS + "content");
                XElement properties = (content != null && content.Element(metadataNS + "properties") != null)
                                    ? content.Element(metadataNS + "properties")
                                    : e.Data.Element(metadataNS + "properties");

                if (properties == null)
                {
                    throw new ODataAtomFormatException("Entity properties not found.");
                }

                EntryProxyObject entity = e.Entity as EntryProxyObject;
                entity.Properties = new Dictionary<string, object>();

                foreach (XElement property in properties.Elements().Where(x => x.Name.Namespace == dataServiceNS))
                {
                    object value = GetPropertyValue(property, metadataNS);
                    entity.Properties.Add(property.Name.LocalName, value);
                }
            }

            private void UpdateRequestUri(object sender, SendingRequestEventArgs e)
            {
                HttpWebRequest originalRequest = e.Request as HttpWebRequest;

                string baseUri;
                string entitySet;
                string entityKey;
                string queryString;
                ValidateServiceUri(originalRequest.RequestUri.OriginalString, out baseUri, out entitySet, out entityKey, out queryString);
                entityKey = Uri.UnescapeDataString(entityKey);
                string requestUri = baseUri + "/" + entitySet + "(" + entityKey.Substring(1, entityKey.Length - 2).Replace("''", "'") + ")";

                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestUri);
                request.Credentials = originalRequest.Credentials;
                request.Timeout = originalRequest.Timeout;
                request.KeepAlive = originalRequest.KeepAlive;
                request.UserAgent = originalRequest.UserAgent;
                request.SendChunked = originalRequest.SendChunked;
                foreach (string key in originalRequest.Headers.AllKeys.Where(k => k != "User-Agent"))
                {
                    request.Headers[key] = originalRequest.Headers[key];
                }

                request.Method = originalRequest.Method;
                e.Request = request;
            }

            private void SignRequest(object sender, SendingRequestEventArgs e)
            {
                this.authScheme.SignRequest(e.Request);
            }
        }
    }
}    